package org.linuxgears.BouncingBall;

import java.awt.*;
import java.awt.geom.Point2D;
import java.util.*;

public class BouncingBallModel implements BallMover {
    public static int WIDTH = 650;
    public static int HEIGHT = 400;
    public static Point DEFAULT_POSITION = new Point(0, 0);
    private Rectangle bounds = new Rectangle(0, 0, WIDTH, HEIGHT);

	private TreeMap<BallRunnable, BallContext> contextMap;
    private BallRunnable activeBallRunnable = null;

	private BallMovingStrategy state;
    
    private Memento savedState;
    private Memento initialState;

    private BouncingBallModel() {
        contextMap = new TreeMap<BallRunnable, BallContext>();
        resume();
        initialState = getMemento();
    }

    private static BouncingBallModel instance = new BouncingBallModel();

    public static BouncingBallModel getInstance() {
        return instance;
    }

    
    public Rectangle getBounds() {
		return bounds;
	}
    
    public BallRunnable addBall(Ball ball) {
        return addBall(ball, (Point)DEFAULT_POSITION.clone());
    }

    public BallRunnable addBall(Ball ball, Point position) {
    	return addBall(ball, position, this);
    }
    
    public BallRunnable addBall(Ball ball, Point position, BallMover mover) {
    	if (null == mover) {
    		mover = this;
    	};
        BallRunnable br = new BallRunnable(ball, mover);
        contextMap.put(br, new BallContext(bounds, position));
        Thread t = new Thread(br);
        t.start();        
        return br;
    }

    public void removeBall(BallRunnable runnable) {
    	if (runnable == null) return;
		contextMap.remove(runnable);
		runnable.interrupt();
		
		// TODO: we would like, if this would be forgotten, be catched by some fantastic assert
		if (activeBallRunnable == runnable) {
			activeBallRunnable = null;
		}
    }
    
    public BallContext getBallContext(Ball ball) {
        BallRunnable br = findBallRunnable(ball);
        if (br == null) return null;
        return contextMap.get(br);
    }

    public BallRunnable findBallRunnable(Ball ball) {
        for (BallRunnable br: contextMap.keySet()) {
            if (br.getBall().equals(ball)) return br;
        }
        return null;
    }

    public void move(Ball ball) {
        this.state.move(ball);
    }

    public Collection<SimpleBallPainter> getPainters() {
        LinkedList<SimpleBallPainter> painters = new LinkedList<SimpleBallPainter>();

        BallPainterFactory bpf = BallPainterFactory.getInstance();
        for (BallRunnable br: contextMap.keySet()) {
            if ( isSelected(br)) {
                painters.add(bpf.createSelectedBallPainter(br.getBall(), contextMap.get(br)));
            } else {
                painters.add(bpf.createBallPainter(br.getBall(), contextMap.get(br)));
            }
        }

        return painters;
    }

    // Selection methods
    public void selectBall(Ball ball) {
        this.activeBallRunnable = findBallRunnable(ball);
    }

    public boolean isSelected(Ball ball) {
        if (ball == null) return false;
        if (activeBallRunnable == null) return false;
        return (activeBallRunnable.getBall() == ball);
    }

    public boolean isSelected(BallRunnable ballRunnable) {
        if (ballRunnable == null) return false;
        if (activeBallRunnable == null) return false;
        return (this.activeBallRunnable == ballRunnable);
    }

    public void selectNextBall() {
        Iterator<BallRunnable> ibr = contextMap.keySet().iterator();
        if (! ibr.hasNext()) return;		// no balls
        
        BallRunnable first = ibr.next();
        if (activeBallRunnable == null) {
            activeBallRunnable = first;
        } else if (isSelected(first) && ibr.hasNext()) {
            activeBallRunnable = ibr.next();
        } else {
            while (ibr.hasNext() && (! isSelected(ibr.next())) ) {
                // skip this runnable
            };

            if (ibr.hasNext()) {
                activeBallRunnable = ibr.next();
            } else {
                activeBallRunnable = first;
            }
        }
    }
    
    public void selectBallAt(Point position) {
    	for (BallRunnable ballRunable: contextMap.keySet()) {
    		if (pointBelongsTo(position, ballRunable.getBall())) {
    			activeBallRunnable = ballRunable;
    			break;
    		}
    	}
    }
	
    public boolean pointBelongsTo(Point position, Ball ball) {
    	BallContext xtc = contextMap.get(findBallRunnable(ball));
    	return (4 * BallTools.distanceSq(position, BallTools.getBallCenterByPosition(ball, xtc.getPosition())) < BallFactory.DEFAULT_BALL_SIZE * BallFactory.DEFAULT_BALL_SIZE );
    }

	public void selectPreviousBall() {
		Iterator<BallRunnable> ibr = contextMap.keySet().iterator();
		if (! ibr.hasNext()) return;		// no balls!
		
		BallRunnable first = ibr.next();
		
		if (null == activeBallRunnable || first == activeBallRunnable) {
			// select last ball
			activeBallRunnable = first;		// may be or may be not, but now it is for sure
			while (ibr.hasNext()) {
				activeBallRunnable = ibr.next();
			}
		} else {
			BallRunnable activeCandidate = first;
			
			while (ibr.hasNext()) {
				BallRunnable next = ibr.next();
				
				if (next == activeBallRunnable) {
					activeBallRunnable = activeCandidate;
					break;
				} else {
					activeCandidate = next;
				}
			}
			
		}
	}
	
    public BallRunnable getActiveBallRunnable() {
		return activeBallRunnable;
	}    

    // Model state control
    public void resume() {
        state = new BallMovingStrategy.Running(this);
    }

    public void pause() {
        state = new BallMovingStrategy.Paused(this);
    }

    public boolean isPaused() {
        return state instanceof BallMovingStrategy.Paused;
    }

    // Save/restore state functionality
    
    public Memento getMemento() {
    	Memento m = new Memento();
    	m.bounds = (Rectangle)bounds.clone();
    	m.activeBallRunnable = activeBallRunnable;
    	m.contextMap = (TreeMap<BallRunnable, BallContext>)contextMap.clone();
    	m.state = state;
    	return m;
    }
    
    public void setMemento(Memento m) {
    	this.bounds = m.bounds;
    	this.activeBallRunnable = m.activeBallRunnable;
    	this.state = m.state;
    	
    	// now we want to kill any BallRunnables that are not in saved context
    	
//    	this.contextMap.keySet().retainAll(m.contextMap.keySet());
    	
    	Set<BallRunnable> assesToRemove = new TreeSet<BallRunnable>(this.contextMap.keySet());
    	assesToRemove.removeAll(m.contextMap.keySet());
    	
    	for (BallRunnable br: assesToRemove) {
    		br.interrupt();
    	}
    	
    	this.contextMap.keySet().retainAll(m.contextMap.keySet());
    	
/*
    	for (BallRunnable br: this.contextMap.keySet()) {
    		System.out.println("# of balls: " + this.contextMap.size() + " should be: " + m.contextMap.size());
    		if (! m.contextMap.keySet().contains(br)) {
    			// kill the runnable
    			br.interrupt();
    			this.contextMap.remove(br);
     		}
    	}
*/    	
    	// TODO: add new balls from memento state  
    	// generally we would also want to recreate any new BallRunnables from memento 
    	// not present in this.contextMap
    }
    
    public void saveState() {
    	savedState = getMemento();
    }
    
    public void restoreState() {
    	setMemento(savedState);
    }
    
    public void resetState() {
    	setMemento(initialState);
    }
    
    // pattern: Memento
    public static class Memento {
    	private Rectangle bounds;
    	private BallRunnable activeBallRunnable;
    	private TreeMap<BallRunnable, BallContext> contextMap;
    	private BallMovingStrategy state;
    }

	public void deleteBallRunnable(BallRunnable selected) {
		// TODO Auto-generated method stub
		
	}

	public void moveSelectedBallTo(Point newPosition) {
		if (activeBallRunnable == null || 
				! (activeBallRunnable.getMover() instanceof InteractiveMovingStrategy) ) 
			return;
		
		InteractiveMovingStrategy ims = (InteractiveMovingStrategy) activeBallRunnable.getMover();
		ims.startMovingTo(activeBallRunnable.getBall(), BallTools.getBallPositionByCenter(activeBallRunnable.getBall(), newPosition));
	}

}
